package tester

import (
	"fmt"
	"io"
	"os"
	"strings"

	"gitee.com/AlexWoo/arpc/alog"
)

var (
	lctx *alog.LogCtx
)

type tokenResultType int

const (
	para_start  tokenResultType = iota // ":"
	para_end                           // ";"
	block_start                        // "{"
	block_end                          // "}"
	conf_end
	conf_error
)

func (t tokenResultType) String() string {
	switch t {
	case para_start:
		return "para_start"
	case para_end:
		return "para_end"
	case block_start:
		return "block_start"
	case block_end:
		return "block_end"
	case conf_end:
		return "conf_end"
	default:
		return "conf_error"
	}
}

type script struct {
	entry string
	steps map[string]*step
}

func parseFile(path string, ctx *alog.LogCtx) (*script, error) {
	f, err := os.Open(path)
	if err != nil {
		return nil, err
	}

	b, err := io.ReadAll(f)
	if err != nil {
		return nil, err
	}

	lctx = ctx

	p, sc, err := parseScript(b)
	if err != nil {
		alog.LogError(lctx, "parse script %s error at %d[%s], error: %s", path, p, string(b[p:]), err.Error())
		return nil, err
	}

	return sc, nil
}

func isSpace(b byte) bool {
	if b == ' ' || b == '\t' || b == '\n' {
		return true
	}

	return false
}

func readToken(b []byte, start int) (tokenResultType, int, string) {
	last := len(b)

	// skip space
	for start != last {
		if isSpace(b[start]) {
			start++
			continue
		} else {
			break
		}
	}

	if start == last {
		return conf_end, start, ""
	}

	end := start
	for {
		switch b[end] {
		case ':':
			return para_start, end, strings.TrimSpace(string(b[start:end]))
		case ';':
			return para_end, end, strings.TrimSpace(string(b[start:end]))
		case '{':
			return block_start, end, strings.TrimSpace(string(b[start:end]))
		case '}':
			return block_end, end, strings.TrimSpace(string(b[start:end]))
		}

		end++
		if end == last {
			return conf_end, end, strings.TrimSpace(string(b[start:end]))
		}
	}
}

func readParaValue(b []byte, start int) (int, string, error) {
	if start == len(b) {
		return start, "", fmt.Errorf("conf end when read para value")
	}

	pr, p, token := readToken(b, start)
	if pr == para_end {
		return p, strings.TrimSpace(token), nil
	}

	return p, "", fmt.Errorf("unexpected token type %s when read para value", pr)
}

func checkStepsIllegal(sc *script) error {
	stps := map[string]bool{}

	for _, st := range sc.steps {
		fstps := st.fs.stps()
		for _, stn := range fstps {
			stps[stn] = true
		}

		estps := st.es.stps()
		for _, stn := range estps {
			stps[stn] = true
		}
	}

	if !stps[stepEnd] {
		return fmt.Errorf("no end in all steps exit")
	}

	for n := range stps {
		if n == stepEnd || n == stepErr {
			continue
		}

		if sc.steps[n] == nil {
			return fmt.Errorf("step exit %s not exist", n)
		}
	}

	return nil
}

func travelStps(sc *script, st *step, stps map[string]bool) {
	stps[st.name] = true

	fsstps := st.fs.stps()
	fsstps = append(fsstps, st.es.stepName)
	for _, stn := range fsstps {
		if stn == stepEnd || stn == stepErr {
			continue
		}

		if stps[stn] { // steps already travelled
			continue
		}

		st := sc.steps[stn]
		travelStps(sc, st, stps)
	}
}

func checkStepsReachable(sc *script) error {
	stps := map[string]bool{}
	unreachstp := []string{}

	st := sc.steps[sc.entry]
	travelStps(sc, st, stps)

	for stn := range sc.steps {
		if !stps[stn] {
			unreachstp = append(unreachstp, stn)
		}
	}

	if len(unreachstp) != 0 {
		return fmt.Errorf("unreachable steps %v", unreachstp)
	}

	return nil
}

func checkSteps(sc *script) error {
	if err := checkStepsIllegal(sc); err != nil {
		return err
	}

	if err := checkStepsReachable(sc); err != nil {
		return err
	}

	return nil
}

// stepname1 { ... }
// stepname2 { ... }
// stepname3 { ... }
// ...
func parseScript(b []byte) (int, *script, error) {
	sc := &script{
		steps: map[string]*step{},
	}

	var pr tokenResultType
	var token string
	var err error
	var st *step
	var p int
	var lastStep *step

	start := 0
	last := len(b)

	for {
		if start == last && sc.entry == "" {
			return p, nil, fmt.Errorf("conf end when parse script")
		}

		pr, p, token = readToken(b, start)
		switch pr {
		case block_start:
			if sc.steps[token] != nil {
				return p, nil, fmt.Errorf("dup step(%s) in script", token)
			}

			start = p + 1

			p, st, err = parseStep(token, b, start)
			if err != nil {
				return p, nil, err
			}

			if sc.entry == "" {
				sc.entry = st.name
			}

			sc.steps[st.name] = st

			start = p + 1

			if lastStep != nil && lastStep.fs == nil {
				lastStep.fs = &nextstep{stepName: st.name}
			}

			lastStep = st

		case conf_end:
			if token != "" {
				return p, nil, fmt.Errorf("unexpected conf end when parse script")
			}

			if st.fs == nil {
				st.fs = defaultEndStep
			}

			if err := checkSteps(sc); err != nil {
				return p, nil, err
			}

			return p, sc, nil

		default:
			return p, nil, fmt.Errorf("unexpected token type %s when parse script", pr)
		}
	}
}
